#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <syslog.h>
#include <time.h>
#include <assert.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdarg.h>
#include <sys/stat.h>
#include <unistd.h>
#include <ctype.h>

#include <jsapi.h>

#include "ircjs.h"
struct _debug debug;
#include "net.h"

#define STACK_CHUNK_SIZE    8192

JSRuntime *rt = NULL;
JSContext *cx = NULL;
JSObject *globalObj = NULL;

JSClass global_class = {
  "global", 0,
  JS_PropertyStub,JS_PropertyStub,JS_PropertyStub,JS_PropertyStub,
  JS_EnumerateStub,JS_ResolveStub,JS_ConvertStub,JS_FinalizeStub
};
  
JSObject* srcobj = NULL;
JSObject* paramobj = NULL;
enum { GO, EXIT } status;

void js_shutdown ();

void fail ( char* msg ) {
  js_shutdown();
  fprintf ( stderr, "%s\n", msg );
  _exit ( 1 );
}

static void js_ErrorReporter(JSContext *cx, const char *message, JSErrorReport *report) {
  if ( strstr ( message, "is not a function" ) ) return;
  fprintf ( stderr, "JS Error: %s\n", message );
  fprintf ( stderr, "%s:%d\n%s\n", report->filename, report->lineno, report->linebuf );
}

void js_init () {
  /* You need a runtime and one or more contexts to do anything with JS. */
  rt = JS_NewRuntime( 64L * 1024L * 1024L );
  if (!rt)
    fail("can't create JavaScript runtime");
  cx = JS_NewContext(rt, 8192);
  if (!cx)
    fail("can't create JavaScript context");
  
  /*
   * The context definitely wants a global object, in order to have standard
   * classes and functions like Date and parseInt.  See below for details on
   * JS_NewObject.
   */
  globalObj = JS_NewObject(cx, &global_class, NULL, NULL);
  if ( !globalObj ) 
    fail ( "can't create global object" );
  JS_SetErrorReporter ( cx, js_ErrorReporter );
  if ( !JS_InitStandardClasses(cx, globalObj) ) fail ( "Initializing standard classes" );

  // Create src and param objects and attach them to the global object
  // so that the GC does not hose them
  srcobj = JS_DefineObject ( cx, globalObj, "_src", NULL, NULL, JSPROP_PERMANENT );
  paramobj = JS_NewArrayObject ( cx, 0, NULL );
  JS_DefineProperty ( cx, globalObj, "_param", OBJECT_TO_JSVAL ( paramobj ), 
		      NULL, NULL, JSPROP_PERMANENT );
}

JSScript* js_load_main_script ( char* filename ) {
  uint32 opts = JS_GetOptions ( cx );
  JS_SetOptions ( cx, opts | JSOPTION_COMPILE_N_GO );
  
  JSScript* script = JS_CompileFile ( cx, globalObj, filename );
  if ( script == NULL ) fail ( "Could not load initial script" );

  jsval rval;
  if ( !JS_ExecuteScript(cx, globalObj, script, &rval ) ) {
    fail ( "Could not exec init script" );
  }
  
  JS_SetOptions ( cx, opts );

  return script;
}

void js_shutdown () {
  /* For each context you've created: */
  if ( cx ) JS_DestroyContext(cx);
  
  /* For each runtime: */
  if ( rt ) JS_DestroyRuntime(rt);
}

char* get_string ( JSObject* obj, char* name ) {
  jsval rval;
  if ( JS_LookupProperty ( cx, obj, name, &rval ) &&
       JSVAL_IS_STRING ( rval ) ) {
    return JS_GetStringBytes ( JS_ValueToString ( cx, rval ) );
  }
  return NULL;
}

JSObject* get_application_object () {
  jsval rval;
  if ( JS_LookupProperty ( cx, globalObj, "app", &rval ) &&
       JSVAL_IS_OBJECT ( rval ) ) {
    return JSVAL_TO_OBJECT ( rval );
  }
  return NULL;
}

JSObject* get_defaults_object () {
  JSObject* app = get_application_object();
  jsval rval;
  if ( app != NULL && JS_LookupProperty ( cx, app, "defaults", &rval ) &&
       JSVAL_IS_OBJECT ( rval ) ) {
    return JSVAL_TO_OBJECT ( rval );
  }
  return NULL;
}

JSObject* get_connection_object ( char* name ) {
  JSObject* app = get_application_object();
  jsval rval;
  jsval rval2;
  if ( name != NULL && app != NULL && 
       JS_LookupProperty ( cx, app, "connections", &rval ) &&
       JSVAL_IS_OBJECT ( rval ) && 
       JS_LookupProperty ( cx, JSVAL_TO_OBJECT ( rval ), name, &rval2 ) &&
       JSVAL_IS_OBJECT ( rval2 ) ) {
    return JSVAL_TO_OBJECT ( rval2 );
  }
  return NULL;
}

char* get_default_port () {
  JSObject* dobj;
  if ( ( dobj = get_defaults_object() ) != NULL ) {
    jsval rval;
    if ( JS_LookupProperty ( cx, dobj, "port", &rval ) &&
	 JSVAL_IS_STRING ( rval ) ) {
      return JS_GetStringBytes ( JS_ValueToString ( cx, rval ) );
    }
  }
  return strdup ( "6667" );
}

char* get_default_username () {
  JSObject* dobj;
  if ( ( dobj = get_defaults_object() ) != NULL ) {
    jsval rval;
    if ( JS_LookupProperty ( cx, dobj, "username", &rval ) &&
	 JSVAL_IS_STRING ( rval ) ) {
      return JS_GetStringBytes ( JS_ValueToString ( cx, rval ) );
    }
  }
  return strdup ( "JsIrC" );
}

char* get_default_realname () {
  JSObject* dobj;
  if ( ( dobj = get_defaults_object() ) != NULL ) {
    jsval rval;
    if ( JS_LookupProperty ( cx, dobj, "realname", &rval ) &&
	 JSVAL_IS_STRING ( rval ) ) {
      return JS_GetStringBytes ( JS_ValueToString ( cx, rval ) );
    }
  }
  return strdup ( "Javascript IRC bot" );
}

char* get_default_nick () {
  JSObject* dobj;
  if ( ( dobj = get_defaults_object() ) != NULL ) {
    jsval rval;
    if ( JS_LookupProperty ( cx, dobj, "nick", &rval ) &&
	 JSVAL_IS_STRING ( rval ) ) {
      return JS_GetStringBytes ( JS_ValueToString ( cx, rval ) );
    }
  }
  return strdup ( "JSirc" );
}

static char* get_current_nick ( char* name ) {
  JSObject* co = get_connection_object ( name );
  if ( co == NULL ) co = get_defaults_object ();
  if ( co ) {
    jsval rval;
    if ( JS_LookupProperty ( cx, co, "current_nick", &rval ) &&
	 JSVAL_IS_STRING ( rval ) ) {
      return JS_GetStringBytes ( JS_ValueToString ( cx, rval ) );
    }
  }
  return strdup ( "SMirc" );
}

void create_connection ( char* name, JSObject* obj ) {
  char* host = get_string ( obj, "host" );
  char* port = get_string ( obj, "port" );
  char* username = get_string ( obj, "username" );
  char* realname = get_string ( obj, "realname" );
  char* nick = get_string ( obj, "nick" );
  if ( host == NULL ) {
    return;
  }
  if ( port == NULL ) port = get_default_port();
  if ( username == NULL ) username = get_default_username ();
  if ( realname == NULL ) realname = get_default_realname ();
  if ( nick == NULL ) nick = get_default_nick ();

  if ( debug.outgoing ) 
    debug_out ( "connecting to %s:%s with: "
		"%s %s %s\n", host, port, nick, username, realname );
  
  create_tcp_connection ( name, host, port );
}

void init_create_connections() {
  // examine the global object
  jsval rval;
  JSObject* app = get_application_object();
  if ( app == NULL ) return;
       
  if ( JS_LookupProperty ( cx, app, "connections", &rval ) &&
       JSVAL_IS_OBJECT ( rval ) ) {
    JSIdArray* ids;
    if ( ( ids = JS_Enumerate ( cx, JSVAL_TO_OBJECT ( rval ) ) ) != NULL ) {
      JSObject* connections = JSVAL_TO_OBJECT ( rval );
      jsint i;
      jsid id;
      for ( i = 0; i < ids->length; i++ ) {
	id = ids->vector[i];
	jsval val;
	JS_IdToValue ( cx, id, &val );
	if ( JSVAL_IS_STRING ( val ) ) {
	  char* conn = JS_GetStringBytes ( JS_ValueToString ( cx, val ) );
	  jsval cval;
	  if ( JS_LookupProperty ( cx, connections, conn, &cval ) &&
	       JSVAL_IS_OBJECT ( cval ) ) {
	    create_connection ( conn, JSVAL_TO_OBJECT ( cval ) );
	  }
	}
      }
    }
  }
}

static void src_add_prop ( char* name, char* val ) {
  JSString* val_str = val == NULL ?
    JS_NewStringCopyN ( cx, "", 0 ) :
    JS_NewStringCopyN ( cx, val, strlen ( val ) );
  JS_DefineProperty ( cx, srcobj, 
		      name, STRING_TO_JSVAL(val_str),
		      NULL, NULL, 
		      JSPROP_ENUMERATE|JSPROP_READONLY );
}

void populate_param_array ( int n, char* params[] ) {
  JS_SetArrayLength ( cx, paramobj, n );
  int i;
  for ( i = 0; i < n; i++ ) {
    JSString* val_str = params[i] == NULL ?
      JS_NewStringCopyN ( cx, "", 0 ) :
      JS_NewStringCopyN ( cx, params[i], strlen ( params[i] ) );
    jsval v = STRING_TO_JSVAL(val_str);
    JS_SetElement ( cx, paramobj, i, &v );
  }
}

void tcp_send ( void*, char* fmt, ... );
int call_js_func ( char* cmd, jsval args[], int nargs, void* conn ) {
  jsval rval;

  if ( JS_CallFunctionName ( cx, globalObj, cmd, nargs, args, &rval ) ) {
    if ( JSVAL_IS_STRING ( rval ) ) {
      JSString* r = JS_ValueToString ( cx, rval );
      char* rr = JS_GetStringBytes ( r );
      tcp_send ( conn, "%s\r\n", rr );
      if ( debug.outgoing ) debug_out ( "sent [%s]\n", rr );
    } else if ( JSVAL_IS_OBJECT ( rval ) ) {
      JSObject* o;
      if ( JS_ValueToObject ( cx, rval, &o ) ) {
	jsuint len = 0;
	if ( JS_IsArrayObject ( cx, o  ) &&
	     JS_GetArrayLength ( cx, o, &len ) && len > 0 ) {
	  // Array
	  int i;
	  for ( i = 0; i < len; i++ ) {
	    jsval r;
	    if ( JS_GetElement ( cx, o, i, &r ) && JSVAL_IS_STRING ( r ) ) {
	      char* rr = JS_GetStringBytes ( JSVAL_TO_STRING ( r ) );
	      tcp_send ( conn, "%s\r\n", rr );
	      //	      fprintf ( stderr, "sent [%s]\n", rr );
	    }
	  }
	} else {
	  // Object
	}
      }
    } else if ( JSVAL_IS_INT ( rval ) ) {
      // int n = JSVAL_TO_INT ( rval );
    } else if ( JSVAL_IS_BOOLEAN ( rval ) ) {
      if ( !JSVAL_TO_BOOLEAN ( rval ) ) {
	tcp_send ( conn, "QUIT :Bye\r\n" );
      }
    } 
    return 1;
  } else {
    //	fprintf ( stderr, "cmd %s unknown\n", cmd );
    return 0;
  }
}

void process_irc_message ( char* src, char* irc_cmds[], int irc_ncmds, char* trailer,
			   void* conn ) {
  char* name = get_tcp_connection_name ( conn );

  jsval args[3];

  src_add_prop ( "connection", name );
  src_add_prop ( "mynick", get_current_nick ( name ) );

  if ( src ) {
    char* nick = src;
    char* host = NULL;
    if ( ( host = strchr ( src, '@' ) ) != NULL )  *host++ = '\0';
    char* user = NULL;
    if ( ( user = strchr ( src, '!' ) ) != NULL ) *user++ = '\0';
	
    src_add_prop ( "nick", nick );
    src_add_prop ( "user", user );
    src_add_prop ( "host", host );
  } 
    

  args[0] = OBJECT_TO_JSVAL ( srcobj );
  
  populate_param_array ( irc_ncmds - 1, &irc_cmds[1] );
  
  args[1] = OBJECT_TO_JSVAL ( paramobj );
  
  JSString* last_arg = trailer == NULL ?
    JS_NewStringCopyN ( cx, "", 0 ) :
    JS_NewStringCopyN ( cx, trailer, strlen ( trailer ) );
  args[2] = STRING_TO_JSVAL ( last_arg );

  char cmd[512];
  sprintf ( cmd, "on_%s_%s", name, irc_cmds[0] );

  if ( call_js_func ( cmd, args, 3, conn ) ) {}
  else {
    sprintf ( cmd, "on_%s", irc_cmds[0] );
    call_js_func ( cmd, args, 3, conn );
  }
}

void connected ( char* name, void* conn ) {
  char cmd[512];
  sprintf ( cmd, "on_%s_connected", name );
  
  if ( call_js_func ( cmd, NULL, 0, conn ) ) {}
  else {
    sprintf ( cmd, "on_connected" );
    call_js_func ( cmd, NULL, 0, conn );
  }
}

/*
void call_js_func_timed () {
  char cmd[512];
  sprintf ( cmd, "timer_60_seconds" );
  
  call_js_func ( cmd, NULL, 0, NULL );
}

void js_func_add_timer_event ( JSContext* ctx,
			       JSObject* obj,
			       uint nargs,
			       jsval *argv,
			       jsval *rval ) {
}

void js_define_functions () {
  // add_timer ( 60 // seconds from now
  //             
  // JS_DefineFunction ( cx, globalObj, "add_timer", js_func_add_timer_event, 1, 0 );
}
*/

void call_timer ( char* name, void* conn ) {
  char cmd[255];
  sprintf ( cmd, "timer_%s", name );
  call_js_func ( cmd, NULL, 0, conn );
}

void init_parser ();
void call_timers();

static char* script_file = "irc.js";

void debug_out ( char* fmt, ... ) {
  va_list l;
  va_start ( l, fmt );
  vfprintf ( stderr, fmt, l );
}

int js_func_debug_out ( JSContext* ctx,
			 JSObject* obj,
			 uint nargs,
			 jsval *argv,
			 jsval *rval ) {
  char buf[4096];
  char tmp[1024];
  buf[0] = '\0';
  int i;
  for ( i = 0; i < nargs; i++ ) {
    if ( JSVAL_IS_STRING ( argv[i] ) ) {
      char* a = JS_GetStringBytes ( JS_ValueToString ( cx, argv[i] ) );
      strcat ( buf, a );
      strcat ( buf, " " );
    } else if ( JSVAL_IS_INT ( argv[i] ) ) {
      sprintf ( tmp, "%d ", JSVAL_TO_INT ( argv[i] ) );
      strcat ( buf, tmp );
    } else if ( JSVAL_IS_BOOLEAN ( argv[i]  ) ) {
      sprintf ( tmp, "%s ", JSVAL_TO_BOOLEAN ( argv[i] ) ? "true" : "false" );
      strcat ( buf, tmp );
    }
  }
  strcat ( buf, "\n" );
  debug_out ( "%s", buf );
  return 1;
}

int js_func_debug_out_off ( JSContext* ctx,
			 JSObject* obj,
			 uint nargs,
			 jsval *argv,
			 jsval *rval ) {
  return 1;
}

int main ( int argc, char* argv[] ) {
  js_init();
  
  if ( JS_DefineFunction ( cx, globalObj, "debug_out", 
			   js_func_debug_out, 1, 0 ) == NULL ) {
    fprintf ( stderr, "Could not define function debug_out\n" );
    exit ( 1 );
  }
  
  JSScript* script = js_load_main_script ( script_file );
  
  JSObject* app = get_application_object();
  if ( app == NULL ) {
    fprintf ( stderr, "no app object\n" );
    exit ( 1 );
  }
  jsval rval;
  if ( JS_LookupProperty ( cx, app, "debug", &rval ) &&
       JSVAL_IS_BOOLEAN ( rval ) && JSVAL_TO_BOOLEAN ( rval ) ) 
    debug.from_script = 1;
  if ( JS_LookupProperty ( cx, app, "debug_incoming", &rval ) &&
       JSVAL_IS_BOOLEAN ( rval ) && JSVAL_TO_BOOLEAN ( rval ) ) 
    debug.incoming = 1;
  if ( JS_LookupProperty ( cx, app, "debug_outgoing", &rval ) &&
       JSVAL_IS_BOOLEAN ( rval ) && JSVAL_TO_BOOLEAN ( rval ) ) 
    debug.outgoing = 1;
  if ( JS_LookupProperty ( cx, app, "debug_system", &rval ) &&
       JSVAL_IS_BOOLEAN ( rval ) && JSVAL_TO_BOOLEAN ( rval ) ) 
    debug.system = 1;

  if ( debug.from_script ) {
    if ( JS_LookupProperty ( cx, app, "debug_file", &rval ) ) {
      if ( JSVAL_IS_STRING ( rval ) ) {
      }
    }
    debug_out ( "debugging on\n" );
  } else {
    JS_DefineFunction ( cx, globalObj, "debug_out", js_func_debug_out_off, 1, 0 );
  }
  
  init_parser();

  init_net();

  init_create_connections();

  struct stat sb;
  time_t mtime = 0;
  if ( !debug.from_script ) daemon ( 1, 0 );
  
  time_t timer_timeout = time(NULL) + 60;
  static int go = 1;
  static int gc_count = 0;
  while ( go ) {
    go = net_dispatch();  // has a 1 second timeout
    stat ( script_file, &sb );
    if ( sb.st_mtime > mtime ) {
      mtime = sb.st_mtime;
      JS_DestroyScript ( cx, script );
      script = js_load_main_script ( script_file );
    }
    if ( time(NULL) > timer_timeout ) {
      call_timers(); // call timers for each connection
      timer_timeout = time(NULL) + 60;
    }
    if ( !gc_count-- ) {
      gc_count = 1000;
      JS_MaybeGC ( cx );
    }
  }

  JS_DestroyScript ( cx, script );
  js_shutdown();
#ifdef DEBUG
  fclose ( debuglog );
#endif
  return 0;
}
